﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using OmronLib.Base;

namespace OmronLib
{
    public class FinsTcp : FinsCommand
    {
        private int pcNode, plcNode;
        private Socket _socket;
        private byte sid = 0;
        private static readonly object lockObj = new object();

        public FinsTcp(int timeout = 3000)
        {
            _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            _socket.ReceiveTimeout = timeout;
        }

        public void Connect(string Ip, int port)
        {
            _socket.Connect(Ip, port);
            pcNode = byte.Parse(Ip.Split('.')[3]);

            byte[] buff = new byte[]
            {
                0x46,0x49,0x4E,0x53,    // FINS HEADER
                0x00,0x00,0x00,0x0C,    // 后续字节长度
                0x00,0x00,0x00,0x01,    // Command
                0x00,0x00,0x00,0x00,    // Status Code

                0x00,0x00,0x00,(byte)pcNode // Client Node Address
            };

            byte[] resp_bytes = this.SendAndReceive(buff);
            byte[] status_code = resp_bytes.Skip(12).Take(4).ToArray();
            string status_str = string.Join("", status_code.Select(b => b.ToString("X2")));
            if (!"00000000".Equals(status_str))
            {
                if (FinsHeaderErrors.Errors.ContainsKey(status_str))
                    throw new Exception(FinsHeaderErrors.Errors[status_str]);
                else
                    throw new Exception("未知错误");
            }
            
            plcNode = resp_bytes[15];
        }

        private byte CreateSID()
        {
            lock (lockObj)
            {
                sid++;
                sid %= 0xFF;
                return sid;
            }
        }

        public byte[] Read(FinsParameter parameter)
        {
            return Read(parameter.Area, parameter.WordAddr, parameter.BitAddr, 1, parameter.DataType);
        }

        public byte[] Read(string variable, int count)
        {
            FinsParameter parameter = GetAddress(variable);
            return Read(parameter.Area, parameter.WordAddr, parameter.BitAddr, (ushort)count, parameter.DataType);
        }

        public void Write(FinsParameter parameter)
        {
            Write(parameter.Data,parameter.Area,parameter.WordAddr, parameter.BitAddr, parameter.DataType);
        }

        public void Write(string variable, byte[] datas)
        {
            FinsParameter parameter = GetAddress(variable);
            parameter.Data = datas;
            Write(parameter);
        }

        public byte[] Read(Area area, ushort wordAddr, byte bitAddr = 0,  ushort count = 1, DataType dataType = DataType.WORD)
        {
            byte a = (byte)area;
            if (dataType == DataType.WORD)
            {
                a += 0x80;
            }
            byte SID = CreateSID();
            byte[] req_bytes = new byte[]
            {
                0x46,0x49,0x4E,0x53,    // FINS HEADER
                0x00,0x00,0x00,0x1A,    // 后续字节长度
                0x00,0x00,0x00,0x02,    // Command
                0x00,0x00,0x00,0x00,    // Status Code

                0x80,
                0x00,0x02,
                0x00,(byte)plcNode,0x00,
                0x00,(byte)pcNode,0x00,
                SID,
                0x01,0x01,
                a,
                (byte)(wordAddr / 256 % 256),(byte)(wordAddr % 256),
                bitAddr,
                (byte)(count / 256 % 256),(byte)(count % 256)
            };

            byte[] resp_bytes = this.SendAndReceive(req_bytes);
            this.CheckResponse(resp_bytes, SID);

            if (dataType == DataType.WORD)
                count *= 2;

            return resp_bytes.Skip(30).Take(count).ToArray();
        }

        public void MultiRead(params FinsParameter[] parameters)
        {
            byte SID = CreateSID();
            List<byte> req_bytes = new List<byte>()
            {
                0x46,0x49,0x4E,0x53,    // FINS HEADER
                0x00,0x00,0x00,0x00,    // 后续字节长度
                0x00,0x00,0x00,0x02,    // Command
                0x00,0x00,0x00,0x00,    // Status Code

                0x80,
                0x00,0x02,
                0x00,(byte)plcNode,0x00,
                0x00,(byte)pcNode,0x00,
                SID,
                0x01,0x04,
            };
            int len = req_bytes.Count - 8;
            foreach (var item in parameters)
            {
                byte a = (byte)item.Area;
                if (item.DataType == DataType.WORD)
                    a += 0x80;
                byte[] wordAddr = new byte[] { (byte)(item.WordAddr / 256 % 256),(byte)(item.WordAddr % 256) };
                byte bitAddr = item.BitAddr;
                len += 4;
            }
            req_bytes[4] = (byte)(len / 256 / 256 / 256 % 256);
            req_bytes[5] = (byte)(len / 256 / 256 % 256);
            req_bytes[6] = (byte)(len / 256 % 256);
            req_bytes[7] = (byte)(len % 256);

            byte[] resp_bytes = this.SendAndReceive(req_bytes.ToArray());
            this.CheckResponse(resp_bytes);

            // 数据的解析
            // 0000(索引位置31) 0x82 xx xx  0xB0  xx xx 
            int index = 31;
            foreach (var item in parameters)
            {
                int data_len = 1;
                if (item.DataType == DataType.WORD)
                    data_len = 2;
                item.Data = resp_bytes.Skip(index).Take(data_len).ToArray();
                index += data_len + 1;
            }
        }

        public void Write(byte[] datas, Area area,int wordAddr,byte bitAddr = 0, DataType dataType = DataType.WORD)
        {
            byte a = (byte)area;
            int count = datas.Length;
            if (dataType == DataType.WORD)
            {
                a += 0x80;
                count /= 2;
            }

            byte SID = CreateSID();
            int len = datas.Length + 0x1A;
            List<byte> req_bytes = new List<byte>
            {
                0x46,0x49,0x4E,0x53,    // FINS HEADER
                // 后续字节长度
                (byte)(len / 256 / 256 / 256 % 256),
                (byte)(len / 256 / 256 % 256),
                (byte)(len / 256 % 256),
                (byte)(len % 256),
                0x00,0x00,0x00,0x02,    // Command
                0x00,0x00,0x00,0x00,    // Status Code

                0x80,
                0x00,0x02,
                0x00,(byte)plcNode,0x00,
                0x00,(byte)pcNode,0x00,
                SID,
                0x01,0x02,
                a,
                (byte)(wordAddr / 256 % 256),(byte)(wordAddr % 256),
                bitAddr,
                (byte)(count / 256 % 256),(byte)(count % 256)
            };
            req_bytes.AddRange(datas);

            byte[] resp_bytes = this.SendAndReceive(req_bytes.ToArray());
            this.CheckResponse(resp_bytes);
        }

        public void PlcRun(RunMode mode = RunMode.RUN)
        {
            byte SID = CreateSID();
            byte[] req_bytes = new byte[]
            {
                0x46,0x49,0x4E,0x53,    // FINS HEADER
                0x00,0x00,0x00,0x17,
                0x00,0x00,0x00,0x02,
                0x00,0x00,0x00,0x00,

                0x80,
                0x00,
                0x02,
                0x00,(byte)plcNode,0x00,
                0x00,(byte)pcNode,0x00,
                SID,
                0x04,0x01,
                0xff,0xff,
                (byte)mode
            };

            byte[] resp_bytes = this.SendAndReceive(req_bytes);
            this.CheckResponse(resp_bytes,SID);
        }

        public void PlcStop()
        {
            byte SID = CreateSID();
            byte[] req_bytes = new byte[]
            {
                0x46,0x49,0x4E,0x53,    // FINS HEADER
                0x00,0x00,0x00,0x14,
                0x00,0x00,0x00,0x02,
                0x00,0x00,0x00,0x00,

                0x80,
                0x00,
                0x02,
                0x00,(byte)plcNode,0x00,
                0x00,(byte)pcNode,0x00,
                SID,
                0x04,0x02
            };

            byte[] resp_bytes = this.SendAndReceive(req_bytes);
            this.CheckResponse(resp_bytes,SID);
        }

        public DateTime GetPlcClock()
        {
            byte SID = CreateSID();
            byte[] req_bytes = new byte[]
            {
                0x46,0x49,0x4E,0x53,    // FINS HEADER
                0x00,0x00,0x00,0x14,
                0x00,0x00,0x00,0x02,
                0x00,0x00,0x00,0x00,

                0x80,
                0x00,
                0x02,
                0x00,(byte)plcNode,0x00,
                0x00,(byte)pcNode,0x00,
                SID,
                0x07,0x01
            };

            byte[] resp_bytes = this.SendAndReceive(req_bytes);
            this.CheckResponse(resp_bytes, SID);

            byte[] date_bytes = resp_bytes.Skip(30).Take(6).ToArray();

            if (!int.TryParse("20" + date_bytes[0].ToString("00"),out int year))
            {
                throw new Exception("时间转换失败");
            }

            if (!int.TryParse(date_bytes[1].ToString("00"),out int month))
            {
                throw new Exception("时间转换失败");
            }

            if (!int.TryParse(date_bytes[2].ToString("00"), out int day))
            {
                throw new Exception("时间转换失败");
            }

            if (!int.TryParse(date_bytes[3].ToString("00"), out int hour))
            {
                throw new Exception("时间转换失败");
            }

            if (!int.TryParse(date_bytes[4].ToString("00"), out int minute))
            {
                throw new Exception("时间转换失败");
            }

            if (!int.TryParse(date_bytes[5].ToString("00"), out int second))
            {
                throw new Exception("时间转换失败");
            }

            DateTime time = new DateTime(year, month, day, hour, minute, second);
            return time;
        }

        public void SetPlcClock(DateTime time)
        {
            byte SID = CreateSID();
            byte[] req_bytes = new byte[]
            {
                0x46,0x49,0x4E,0x53,    // FINS HEADER
                0x00,0x00,0x00,0x19,
                0x00,0x00,0x00,0x02,
                0x00,0x00,0x00,0x00,

                0x80,
                0x00,
                0x02,
                0x00,(byte)plcNode,0x00,
                0x00,(byte)pcNode,0x00,
                SID,
                0x07,0x02,
                Convert.ToByte(time.Year.ToString(),16),
                Convert.ToByte(time.Month.ToString(),16),
                Convert.ToByte(time.Day.ToString(),16),
                Convert.ToByte(time.Hour.ToString(),16),
                Convert.ToByte(time.Minute.ToString(),16)
            };

            byte[] resp_bytes = this.SendAndReceive(req_bytes);
            this.CheckResponse(resp_bytes, SID);
        }

        public void GetCPUInfo()
        {
            byte SID = CreateSID();
            byte[] req_bytes = new byte[]
            {
                0x46,0x49,0x4E,0x53,    // FINS HEADER
                0x00,0x00,0x00,0x1A,
                0x00,0x00,0x00,0x02,
                0x00,0x00,0x00,0x00,

                0x80,
                0x00,
                0x02,
                0x00,(byte)plcNode,0x00,
                0x00,(byte)pcNode,0x00,
                SID,
                0x06,0x01
            };

            byte[] resp_bytes = this.SendAndReceive(req_bytes);
            this.CheckResponse(resp_bytes, SID);

            byte status = resp_bytes[31];
            byte mode = resp_bytes[32];

            string error_code = string.Join("", resp_bytes.Skip(38).Take(2).Select(b => b.ToString("X2")));
            if (!"0000".Equals(error_code))
            {
                byte[] error_msg_bytes = resp_bytes.Skip(40).ToArray();
                string error_msg = Encoding.UTF8.GetString(error_msg_bytes);
                throw new Exception(error_msg);
            }
        }

        public byte[] SendAndReceive(byte[] bytes)
        {
            _socket.Send(bytes);
            byte[] resp_head = new byte[8];
            int count = _socket.Receive(resp_head,0,8,SocketFlags.None);
            if (count == 0) return null;

            byte[] len_bytes = resp_head.Skip(4).ToArray();
            if(BitConverter.IsLittleEndian)
                Array.Reverse(len_bytes);
            int res_len = BitConverter.ToInt32(len_bytes, 0);

            byte[] resp_body = new byte[res_len];
            _socket.Receive(resp_body, 0, res_len, SocketFlags.None);

            List<byte> resp_bytes = new List<byte>();
            resp_bytes.AddRange(resp_head);
            resp_bytes.AddRange(resp_body);
            return resp_body;
        }

        private void CheckResponse(byte[] resp_bytes,byte SID)
        {
            byte[] standard_head = new byte[] { 0x46, 0x49, 0x4E, 0x53 };

            if (!resp_bytes.Take(4).SequenceEqual(standard_head))
                throw new Exception("数据头不是FINS或ASCII格式");

            byte[] status_code = resp_bytes.Skip(12).Take(4).ToArray();
            string status_str = string.Join("", status_code.Select(b => b.ToString("X2")));
            if (FinsHeaderErrors.Errors.ContainsKey(status_str))
                throw new Exception(FinsHeaderErrors.Errors[status_str]);

            if (resp_bytes[25] != SID)
                throw new Exception("Session ID 不一致");

            string end_status = resp_bytes[28].ToString("X2") + resp_bytes[29].ToString("X2");
            if (!"0000".Equals(end_status))
            {
                if (FINSResponseErrors.Errors.ContainsKey(end_status))
                    throw new Exception(FINSResponseErrors.Errors[end_status]);
                else
                    throw new Exception("未知错误");
            }
        }

        public override void Close()
        {
            if (_socket == null || !_socket.Connected) return;
            _socket.Disconnect(true);
            _socket.Close();
            _socket.Dispose();
        }
    }
}
